﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NetfractionHub.RemoteMachine;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Collections.Concurrent;

namespace NetfractionHub.Network
{
	public class ClientConnection
	{
		private Socket _clientSocket;
		private NetworkStream _networkStream;
		private ConnectionState _state;
		private SslStream _secureStream;
		private bool _isSecure;

		private MessageQueueManager _sendManager;
		private BlockingCollection<byte[]> _sendQueue = new BlockingCollection<byte[]>();
		private bool _pendingSend;

		static X509Certificate2 _serverCertificate = null;
		private bool mMessageIncomplete;
		private string _incompleteMessageFrame;
		private Timer _connectTimer;

		private const int STREAM_BUFFER_SIZE = 1024;//16384;
		private const int STREAM_BUFFER_COUNT = 16;
		private ArraySegment<byte> _buffer;
		private ConcurrentStack<int> _freeBuffers;

		private Node _parent;

		private struct StreamState
		{
			public byte[] buffer;
			public int bufferIndex;
		}

		public ClientConnection(Socket clientSocket, Node parent)
		{
			_parent = parent;

			_buffer = NetfractionHubService.SocketBuffer.CheckOut();
			_freeBuffers = new ConcurrentStack<int>(Enumerable.Range(1, STREAM_BUFFER_COUNT));

			_sendManager = new MessageQueueManager(new Action<string>(FinalSend));
			
			if (clientSocket != null)
			{
				//long c1 = 0, c2 = 0, c3 = 0, c4 = 0, c5 = 0, c6 = 0, c7 = 0;

				//if (NetfractionHubService.SocketBuffer.AvailableBuffers < 7700)
				//    c1 = System.GC.GetTotalMemory(true);

				//if (NetfractionHubService.SocketBuffer.AvailableBuffers < 7700)
				//    c2 = System.GC.GetTotalMemory(true);

				this._clientSocket = clientSocket;
				_networkStream = new NetworkStream(_clientSocket, true);

				//if (NetfractionHubService.SocketBuffer.AvailableBuffers < 7700)
				//    c3 = System.GC.GetTotalMemory(true);

				_isSecure = Settings.HubSettings.HubConnection.ADCS.IsEnabled && (parent.Identity.NodeType & IdentityNodeTypes.Bot) != IdentityNodeTypes.Bot;
				if (_isSecure)
				{
					_sendManager = new MessageQueueManager(new Action<string>(FinalSend), 1);

					string certPath = NetfractionHub.Network.Protocol.ADCS.Security.Certificate.GetCertificatePath();
					if (!File.Exists(certPath))
						Netfraction.Common.Logging.LogManager.WriteEntry("ADCS Hub Certificate verification or creation failed.",
							Netfraction.Common.Logging.LogEventId.TLSCertificateNotFound, Netfraction.Common.Logging.LogLevel.Basic);

					_serverCertificate = new X509Certificate2(certPath);

					System.Security.Cryptography.AsymmetricAlgorithm privKey = NetfractionHub.Network.Protocol.ADCS.Security.Certificate.GetPrivateKey();

					_serverCertificate.PrivateKey = privKey;

					try
					{
						_secureStream = new SslStream(_networkStream, false, new RemoteCertificateValidationCallback(NetfractionHub.Network.Protocol.ADCS.Security.Verification.RemoteCertificateValidationCallback));
						_secureStream.AuthenticateAsServer(_serverCertificate, true, System.Security.Authentication.SslProtocols.Tls, true);
					}
					catch (System.Security.Authentication.AuthenticationException authEx)
					{
						Console.WriteLine(authEx.Message);	// check authEx.InnerException
						Disconnect();
					}
					catch (IOException)
					{

						Disconnect();	// This would happen if a client tries to connect without TLS.
					}
				}

				_state = ConnectionState.Protocol;
				_incompleteMessageFrame = string.Empty;
				//if (NetfractionHubService.SocketBuffer.AvailableBuffers < 7700)
				//    c4 = System.GC.GetTotalMemory(true);

				//Console.WriteLine("remote conn: {0}", _clientSocket.RemoteEndPoint.ToString());
				//if (NetfractionHubService.SocketBuffer.AvailableBuffers < 7700)
				//    c5 = System.GC.GetTotalMemory(true);

				_connectTimer = new Timer(CheckConnectionTimeout, null, Settings.HubSettings.HubConnection.IdentifyTimeout * 1000, System.Threading.Timeout.Infinite);

				//if (NetfractionHubService.SocketBuffer.AvailableBuffers < 7700)
				//    c6 = System.GC.GetTotalMemory(true);


				//if (c1 > 0)
				//{
				//    Console.WriteLine(c1);
				//}
				
				//ThreadPool.QueueUserWorkItem(BeginAsyncStreamRead);

				BeginAsyncStreamRead(null);
			}
		}

		private void CheckConnectionTimeout(object state)
		{
			_connectTimer.Dispose();

			lock (this)
			{
				if (this._state == ConnectionState.Disconnected)
				{
					ConnectionManager.RemoveDisconnectedNode(this._parent);
					return;
				}
				if (this._state != ConnectionState.Normal && this._state != ConnectionState.Data)
				{
					Console.WriteLine("kicking bad attempt at connecting");
					this.Disconnect();
				}
			}
		}

		/// <summary>
		/// Return Local EndPoint
		/// </summary>
		public IPEndPoint LocalEndPoint
		{
			get
			{
				if (_clientSocket != null && _clientSocket.Connected)
					return _clientSocket.LocalEndPoint as IPEndPoint;
				else
					return null;
			}
		}

		/// <summary>
		/// Return Remote EndPoint
		/// </summary>
		public IPEndPoint RemoteEndPoint
		{
			get
			{
				if (_clientSocket != null && _clientSocket.Connected)
					return _clientSocket.RemoteEndPoint as IPEndPoint;
				else
					return null;
			}
		}

		public ConnectionState State
		{
			get { return _state; }
			set
			{
				_state = value;
			}
		}

		private void BeginAsyncStreamRead(object state)
		{
			StreamState streamState = new StreamState();
			//streamState.buffer = _buffer;

			try
			{
				/*if (_isSecure && _secureStream != null)
					_secureStream.BeginRead(streamState.buffer, 0, STREAM_BUFFER_SIZE, new AsyncCallback(EndAsyncStreamRead), streamState);
				else
					_networkStream.BeginRead(streamState.buffer, 0, STREAM_BUFFER_SIZE, new AsyncCallback(EndAsyncStreamRead), streamState);*/
				bool foundBuffer = false;
				while (!foundBuffer)
				{
					foundBuffer = _freeBuffers.TryPop(out streamState.bufferIndex);
				}

				if (_isSecure && _secureStream != null)
					_secureStream.BeginRead(_buffer.Array, (streamState.bufferIndex - 1) * STREAM_BUFFER_SIZE, STREAM_BUFFER_SIZE, new AsyncCallback(EndAsyncStreamRead), streamState);
				else
					_networkStream.BeginRead(_buffer.Array, (streamState.bufferIndex - 1) * STREAM_BUFFER_SIZE, STREAM_BUFFER_SIZE, new AsyncCallback(EndAsyncStreamRead), streamState);

			}
			catch (ObjectDisposedException)
			{
				// this call is redundant, because the network connection no longer exists
				Disconnect();
			}
			catch (Exception e)
			{
				Disconnect();
			}
		}

		private void EndAsyncStreamRead(IAsyncResult asyn)
		{
			//byte[] buffer = null;

			int bytesReceived = 0;

			try
			{
				bytesReceived = _isSecure ? _secureStream.EndRead(asyn) : _networkStream.EndRead(asyn);
				//buffer = ((StreamState)asyn.AsyncState).buffer;
			}
			catch (Exception)
			{
				Disconnect();
			}

			// Check number of bytes recieved
			if (bytesReceived > 0)
			{
				// Recieve Data From Buffer
				Task.Factory.StartNew(() => Receive(((StreamState)asyn.AsyncState).bufferIndex, bytesReceived));

				// loop stream reading
				Task.Factory.StartNew(() => BeginAsyncStreamRead(null));
			}
			else
			{
				// remote node disconnected gracefully
				//this.mState = ConnectionState.Disconnected;
				Disconnect();
			}
		}

		public void Receive(int bufferIndex, int receivedBytesCount)
		{
			if (receivedBytesCount == 0)
				return;

			//Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, receivedBytesCount));
			int bytesLeftToProcess = receivedBytesCount;

			while (bytesLeftToProcess > 0)
			{
				// Process raw string into ProtocolMessages
				string messageFrame = string.Empty;

				string tempStr = string.Empty;
				int messageLength = 0;
				int bufferOffset = (bufferIndex - 1) * STREAM_BUFFER_SIZE;
				int startOfMessage = bufferOffset + receivedBytesCount - bytesLeftToProcess;
				int endOfMessage = bufferOffset + receivedBytesCount;
				bool endFound = false;
				for (int i = startOfMessage; i < bufferOffset + receivedBytesCount; i++)
				{
					if (_buffer.Array[i] == 0x0a)
					{
						endOfMessage = i;
						endFound = true;
						break;
					}
				}

				messageLength = endOfMessage - startOfMessage + (endFound ? 1 : 0);
				tempStr = Protocol.ProtocolMessage.DecodeMessage(_buffer.Array, startOfMessage, messageLength);

				Array.Clear(_buffer.Array, bufferOffset, STREAM_BUFFER_SIZE);
				_freeBuffers.Push(bufferIndex);

				if (mMessageIncomplete) // previous message was incomplete
				{
					messageFrame = _incompleteMessageFrame;
					mMessageIncomplete = false;
					_incompleteMessageFrame = string.Empty;
				}

				if (!endFound)  // this message is incomplete since it doesn't end with a newline character
				{
					mMessageIncomplete = true;

					_incompleteMessageFrame = messageFrame + tempStr;
					return;
				}
				else   // the entire message, or the remainder of it, is in this frame
				{
					messageFrame += tempStr;
				}
				bytesLeftToProcess -= messageLength;

				// Loop All ProtocolMessages
				foreach (Protocol.ProtocolMessage protocolMessage in Protocol.ProtocolMessage.ProcessIncomingMessages(messageFrame))
				{
					// Notify Handlers
					OnMessageReceive(protocolMessage);
				}
			}
		}

		protected void OnMessageReceive(Protocol.ProtocolMessage message)
		{
			// Log
			//log.Debug("[" + this.ToString() + "] Message Received: " + protocolMessage.ToString());
			//System.Diagnostics.Debug.WriteLine(message.ToString());

			// Make sure the node is identifying itself correctly, and not trying to impersonate other users
			if (CheckCurrentNodeOwnsSessionID(message))
			{
				List<Protocol.ProtocolFeature> featuresSupportingMessage = new List<Protocol.ProtocolFeature>();
				List<Protocol.ProtocolFeature> featuresSupportingMessageExtension = new List<Protocol.ProtocolFeature>();
				foreach (Protocol.ProtocolFeature f in this._parent.Features.Values)
				{
					if (f.SupportsActionOfType(message.MessageCode, Protocol.FeatureActionType.Message))
						featuresSupportingMessage.Add(f);
					else if (f.SupportsActionOfType(message.MessageCode, Protocol.FeatureActionType.MessageExtension))
						featuresSupportingMessageExtension.Add(f);
				}
				
				// Call ProtocolFeature handlers
				foreach (Protocol.ProtocolFeature feat in featuresSupportingMessage)
				{
					feat.HandleFeatureMessage(this._parent, message, featuresSupportingMessageExtension);
				}
			}
			else
			{
				// The node has sent an incorrect Session ID. This possibly should be a kickable offence, or maybe add a configuration variable and logging.
				return;
			}
		}

		private bool CheckCurrentNodeOwnsSessionID(NetfractionHub.Network.Protocol.ProtocolMessage message)
		{
			// A Session ID is sent with Broadcast, Direct, Echo, and Feature messages.
			if (message.MessageType == NetfractionHub.Network.Protocol.ProtocolMessage.MessageTypeEnum.Broadcast
				|| message.MessageType == NetfractionHub.Network.Protocol.ProtocolMessage.MessageTypeEnum.Direct
				|| message.MessageType == NetfractionHub.Network.Protocol.ProtocolMessage.MessageTypeEnum.Echo
				|| message.MessageType == NetfractionHub.Network.Protocol.ProtocolMessage.MessageTypeEnum.Feature)
			{
				if (message.SessionId == this._parent.SessionId)
					return true;
				else
					return false;
			}
			else
			{
				return true; // No Session ID is passed identifying the originator for other message types, so just return true.
			}
		}

		/// <summary>
		/// Send Message
		/// </summary>
		public void Send(string message)
		{
			Send(message, false);
		}

		public void Send(string message, bool unparsed)
		{
			if (message.Length == 0)
				return;

			if (unparsed)
			{
				_sendManager.MessageQueue.Add(message);

				//BeginAsyncStreamWrite(new ArraySegment<byte>(popped.Array, 0, reqBytes + 1));
				//popped.Array
			}
			else
			{
				foreach (Protocol.ProtocolMessage protocolMessage in Protocol.ProtocolMessage.ProcessIncomingMessages(message))
				{
					Send(protocolMessage);
				}
			}
		}

		public void Send(Protocol.ProtocolMessage message)
		{
			if (message == null)
				return;

			Send(message.ToString(), true);
		}

		public void FinalSend(string message)
		{
			//string val = message.Trim() + "\n";
			int reqBytes = System.Text.Encoding.UTF8.GetByteCount(message);
			ArraySegment<byte> popped = NetfractionHubService.SocketBuffer.CheckOut();
			System.Text.Encoding.UTF8.GetBytes(message, 0, message.Length, popped.Array, 0);
			//BeginAsyncStreamWrite(encodedMessage);
			popped.Array[reqBytes] = 0x0a;

			BeginAsyncStreamWrite(popped.Array, reqBytes + 1);
		}

		/// <summary>
		/// Start Asynchronous Stream Write
		/// </summary>    
		private void BeginAsyncStreamWrite(byte[] buffer, int length)
		{
			if (_clientSocket == null || buffer == null)
				return;

			// Initialize StreamState object
			//StreamState streamState = new StreamState();
			//streamState.buffer = buffer;

			try
			{
				// Begin Async Stream Write
				// .NET 2.0 SSL Stream issues when sending multiple async packets
				// http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=124213&SiteID=1

				if (_isSecure)
				{
					_sendManager.CanEnumerate = false;
					_secureStream.BeginWrite(buffer, 0, length, EndAsyncStreamWrite, null);
				}
				else
					_networkStream.BeginWrite(buffer, 0, length, EndAsyncStreamWrite, null);
			}
			catch (Exception)
			{
				// TODO: should queue a disconnect here. it would avoid a stackoverflow exception on a mass-disconnect
				Disconnect();
			}
		}

		/// <summary>
		/// End Asynchronous Stream Write
		/// </summary>    
		private void EndAsyncStreamWrite(IAsyncResult asyn)
		{
			try
			{
				// End Async Stream Write    
				if (_isSecure)
				{
					_secureStream.EndWrite(asyn);
					_sendManager.CanEnumerate = true;
				}
				else
					_networkStream.EndWrite(asyn);

				if (asyn != null && asyn.AsyncState is ArraySegment<byte>)
					NetfractionHubService.SocketBuffer.CheckIn((ArraySegment<byte>)asyn.AsyncState);

				if (asyn != null)
					asyn.AsyncWaitHandle.Close();
			}
			catch (Exception)
			{
				Disconnect();
			}
		}
		
		public void Disconnect()
		{
			if (_secureStream != null)
				_secureStream.Close();
			else if (_networkStream != null)
				_networkStream.Close(); // Close Stream
			else
				if (_clientSocket != null)
					_clientSocket.Close(); // Close Socket

			NetfractionHubService.SocketBuffer.CheckIn(_buffer);

			// Set Connection State
			if (this._state == ConnectionState.Disconnected)
			{
				Task.Factory.StartNew(() => ConnectionManager.RemoveDisconnectedNode(this._parent));
				return;
			}
			else if (this._state != ConnectionState.Normal && this._state != ConnectionState.Data)
			{
				ConnectionManager.DisconnectNode(this._parent);
				_state = ConnectionState.Disconnected;
			}
			else if (this._state == ConnectionState.Normal)
			{
				ConnectionManager.DisconnectNode(this._parent);
				_state = ConnectionState.Disconnected;
			}
		}
	}
}